#region License, Terms and Conditions
//
// Prawks - Copyright (c) 2007 Atif Aziz. All rights reserved.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 2.1 of the License, or (at your option)
// any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
//
#endregion

namespace Prawks.CodeProvisioningCompiler
{
    using System;
    using System.Reflection;
    using System.Reflection.Emit;
    using System.Diagnostics;
    using Prawks.CodeProvisioning;
    
    using Path = System.IO.Path;
    using File = System.IO.File;
    using FileStream = System.IO.FileStream;
    using ArrayList = System.Collections.ArrayList;
    using CultureInfo = System.Globalization.CultureInfo;

    sealed class Compiler
    {
        private readonly CompilerOptions _options;
        private readonly Assembly _baseAssembly;

        public static void Compile(CompilerOptions options)
        {
            Debug.Assert(options != null);

            Assembly baseAssembly = Assembly.LoadFrom(options.BaseAssemblyPath);

            if (options.OutputDirectory.Length == 0)
                options.OutputDirectory = Path.GetDirectoryName(baseAssembly.Location);

            Compiler compiler = new Compiler(options, baseAssembly);

            compiler.Compile();
        }

        private Compiler(CompilerOptions options, Assembly baseAssembly)
        {
            Debug.Assert(options != null);
            Debug.Assert(baseAssembly != null);

            _baseAssembly = baseAssembly;
            _options = options;
        }

        private void Compile()
        {
            AssemblyName baseAssemblyName = _baseAssembly.GetName();

            //
            // Check if the base assembly references the Prawks.CodeProvisioning 
            // assembly, and if so, whether it reference the same version as being used 
            // by the compiler or not. Since the method used here is not completely 
            // foolproof (being based on the simple names of the assemblies), we only 
            // issue warnings.
            //

            AssemblyName compilerLibReference = typeof(IMethodImpl).Assembly.GetName();
            AssemblyName baseLibReference = GetAssemblyReferenceByName(_baseAssembly, compilerLibReference.Name);

            if (baseLibReference == null)
            {
                Console.WriteLine(string.Format("WARNING: The '{0}' assembly does not appear to reference the '{1}' assembly. Compilation may either fail or not produce the expected results.\n",
                    _baseAssembly.FullName, compilerLibReference.FullName));
            }
            else if (string.CompareOrdinal(compilerLibReference.FullName, baseLibReference.FullName) != 0)
            {
                Console.WriteLine(string.Format("WARNING: The reference to assembly name '{0}' in the '{1}' assembly appears suspicious. The compiler is expecting a reference to the '{2}' assembly, but has instead found a reference to the '{3}' assembly. Comilation may either fail or not produce the expected results.\n",
                    compilerLibReference.Name, _baseAssembly.FullName, compilerLibReference.FullName, baseLibReference.FullName));
            }

            //
            // Build a list of base types available for compilation from
            // the source assembly.
            //

            Type[] baseTypes = GetBaseTypes();

            if (baseTypes.Length == 0)
            {
                throw new ProgramException(
                    string.Format("The '{0}' assembly does not appear to contain any types that can be compiled.",
                    baseAssemblyName.FullName));
            }

            //
            // Create a dyamic assembly and module where the compiled types will go.
            //

            AssemblyName implAssemblyName = new AssemblyName();

            implAssemblyName.Name = _options.OutputAssemblyName.Length != 0 ?
                _options.OutputAssemblyName : (baseAssemblyName.Name + "Impl");

            if (_options.Version.Length != 0)
                implAssemblyName.Version = new Version(_options.Version);

            if (_options.Culture.Length != 0)
                implAssemblyName.CultureInfo = new CultureInfo(_options.Culture);

            if (_options.KeyFile.Length != 0)
            {
                using (FileStream keyStream = File.OpenRead(_options.KeyFile))
                    implAssemblyName.KeyPair = new StrongNameKeyPair(keyStream);
            }
            else if (_options.KeyName.Length != 0)
            {
                implAssemblyName.KeyPair = new StrongNameKeyPair(_options.KeyName);
            }

            string implDllName = implAssemblyName.Name + ".dll";
            
            AssemblyBuilder implAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(implAssemblyName, AssemblyBuilderAccess.Save, _options.OutputDirectory);
            ModuleBuilder implModule = implAssembly.DefineDynamicModule(implDllName, implDllName);

            //
            // Compile each base type into n concrete implementation type.
            // CodeProvisionException exceptions are caught and the loop tries to
            // get as far as possible.
            //

            int errorsCount = 0;
            int typesCompiledCount = 0;

            DateTime compilationStartTime = DateTime.Now;

            foreach (Type baseType in baseTypes)
            {
                string newTypeName = TypeNameMangler.Default(baseType.FullName);

                Console.WriteLine("Compiling {0} -> {1}", baseType.FullName, newTypeName);

                try
                {
                    TypeImplCompiler.Compile(baseType, newTypeName, implModule);
                    typesCompiledCount++;
                }
                catch (CodeProvisionException e)
                {
                    Console.Error.WriteLine("Error: " + e.Message);
                    errorsCount++;
                }
            }

            DateTime compilationEndTime = DateTime.Now;

            //
            // If there were errors in the previous step then abort with 
            // raising an exception here. Otherwise, save the new assembly.
            //

            if (errorsCount != 0)
            {
                throw new ProgramException("Error compiling one or more types.");
            }

            Console.WriteLine(string.Format(
                "\n{0} type(s) compiled in {1} second(s)", 
                (typesCompiledCount).ToString("#,0"),
                (compilationEndTime - compilationStartTime).TotalSeconds.ToString("#,0.0000")));

            implAssembly.Save(implDllName);

            //
            // Run the verifier on the saved assembly, if requested.
            //

            if (_options.VerifyAssembly)
            {
                string implAssemblyDllPath = Path.Combine(_options.OutputDirectory, implDllName);
                
                if (implAssemblyDllPath.IndexOf(' ') != -1)
                    implAssemblyDllPath = '"' + implAssemblyDllPath + '"';

                using (Process verifier = new Process())
                {
                    Console.WriteLine(string.Format("\nStarting verification ..."));

                    verifier.StartInfo.UseShellExecute = false;
                    verifier.StartInfo.FileName = _options.VerifierPath;
                    verifier.StartInfo.Arguments = implAssemblyDllPath;
                    verifier.StartInfo.RedirectStandardOutput = true;
                    
                    verifier.Start();

                    string line;
                    while ((line = verifier.StandardOutput.ReadLine()) != null)
                        Console.WriteLine(line);

                    verifier.WaitForExit();
                }
            }
        }

        private static AssemblyName GetAssemblyReferenceByName(Assembly sourceAssembly, string simpleName)
        {
            Debug.Assert(sourceAssembly != null);
            Debug.Assert(simpleName != null);
            Debug.Assert(simpleName.Length != 0);

            foreach (AssemblyName referencedName in sourceAssembly.GetReferencedAssemblies())
            {
                if (string.Compare(referencedName.Name, simpleName, true, CultureInfo.InvariantCulture) == 0)
                    return referencedName;
            }

            return null;
        }

        private Type[] GetBaseTypes()
        {
            ArrayList baseTypes = new ArrayList();
            ArrayList baseMembers = new ArrayList();
            Type implAttribute = typeof(MemberImplAttribute);

            //
            // Get all the exported types and process only those that
            // are marked as abstract.
            //
            
            foreach (Type type in _baseAssembly.GetExportedTypes())
            {
                if (!type.IsAbstract)
                    continue;

                //
                // Scan all properties and methods of the type for
                // the implementation attribute.
                //

                baseMembers.Clear();
                baseMembers.AddRange(type.GetProperties());
                baseMembers.AddRange(type.GetMethods());

                foreach (MemberInfo member in baseMembers)
                {
                    if (Attribute.IsDefined(member, implAttribute))
                    {
                        baseTypes.Add(type);
                        break;
                    }
                }
            }

            return (Type[]) baseTypes.ToArray(typeof(Type));
        }
    }
}
